﻿/*
程序地址空间
    程序地址空间，并不是内存 ----> 进程地址空间（虚拟地址空间）  是一个系统的概念，不是语言层的概念
           pid_t id = fork();
          //子进程  修改数据
          if(id==0)
          {
             while(1)
             {
               printf("子进程：gval:%d,&gval:%p,pid:%d,ppid:%d\n",gval,&gval,getpid(),getppid());
               sleep(1);
               gval++;    //只有子进程对数据进行修改（但是父进程看不到修改   因为具有写时拷贝）
             }
          }

          //父进程  同样的事但是不修改数据
          else
          {
               while(1)
               {
                  printf("父进程：gval:%d,&gval:%p,pid:%d,ppid:%d\n",gval,&gval,getpid(),getppid());
                  sleep(1);
               }
          }
    上面程序验证，发现二者的内存是一样的，子数据一直在修改，父数据不变，所以他们不是内存地址，而是虚拟地址
     C/C++指针用到的地址，全部都是虚拟地址！进程具有独立性，写时拷贝（单独拷贝一份）

下面的实际是一个 struct mm_struct的一个数据结构
     前面的是3G   用户空间
     正文代码  初始化数据  未初始化数据  堆  共享区  栈  命令行参数环境变量               内核空间（1G）
                          低地址   ------->   高地址
    (* 不是物理内存 *)			  4G（32位）   2^32个地址  一个字节1字节   4G
一个进程，一个虚拟地址空间

在进程所对的PCB中  有一个task_struct 内部包含一个struct mm_struct* struct


要把程序加载到内存需要两件事情：
    1.在虚拟地址空间中申请指定大小的空间
    2.加载程序，申请物理空间
    3.1和2  页表进行映射
反向就是：物理地址转化为虚拟地址，虚拟地址提供给上层用户使用    在虚拟地址中申请指定空间大小（调整区域划分）

mm_struct 
    1.开辟空间
    2.初始化的值，从加载的时候进行初始化

虚拟地址空间是一个结构体，不是内存，上面都是每个区域的起始地址和结束地址，有一个页表会把虚拟地址和结束地址的映射关系构建好
这样就可以进行访问，地址空间就是一个结构体变量



当定义一个全局变量：g_val，会在内存上有一个变量（物理地址），也会在地址空间上有一个四字节的全局变量，这个全局变量的起始 称之为 虚拟地址
操作系统在每一个进程创建的时候，为每一个进程创建页表，页表左边填充左边虚拟地址，右边存储物理内存地址
* 页表：页表是用来做虚拟地址和物理地址映射的

 task_struct是拷贝自 父进程
 父进程有虚拟地址空间，子进程也有虚拟地址空间（拷贝父进程的虚拟地址）     也有自己对应的页表（拷贝自父进程）
 浅拷贝，指向同一块物理内存


 子进程继承了父进程的虚拟地址和页表等，但是子进程需要修改内容的时候，就会写时拷贝，会开辟新的物理内存，把对应的映射关系进行修改
 就是写时拷贝

 fork写时拷贝
 为什么return 会返回两次？既等于又大于0
 变量是父进程定义的，return就是修改变量，发生写时拷贝，父和子拿着同一变量的虚拟地址，但是底层映射到了不同的物理地址

 
 需要管理内容 ---> 先描述再组织
 虚拟地址空间本质：本质就是一个数据结构   struct mm_struct{   };   就是一个结构体对象
 就是在内核当中（操作系统中）给进程创建的结构体对象

 mm_struct 应该包含内容： long code_start ;    long code_end;    long init-start，init_end
                          long uninit_start,uninita_end   ......
地址空间是一个结构体，每一个结构体里都是每一个区域的开始虚拟地址，结束虚拟地址      
对于区域的调整，只需要对整数变量进行加或减就可以 ----> 结构体中的整数进行加或者减

什么是区域划分？
    区域划分只需要确认区域的开始和结束即可!   



用户拿着虚拟地址访问某个变量 ---> 需要把虚拟地址转化成为物理地址 
操作系统OS 查找页表

页表中不仅有虚拟地址  物理地址  也有对应的权限
为什么要有虚拟地址空间和页表？
    1.将地址从无序变为有序  --->  有页表建立映射关系
    2.地址转换过程中，也可以对地址和操作进行合法判定，进而进行保护物理内存
    3.让进程管理和内存管理进行解耦

问题:为什么在字符常量区写入会崩溃？
答案：因为在查找页表的时候，权限拦截了


一些问题：
    1.可以不加载代码和数据，只有task_struct,mm_struct,页表
    2.创建进程，先有task_struct   mm_struct（现有内核数据结构PCB）    然后才有代码和数据
    3.进程挂起，先找到对应的进程：S状态，把页表清空，把所有数据全部换出到swap分区中，只保留进程管理部分，内存部分不保留
      所以挂起的时候，进程是仍然存在的



疑问扩展：堆区，是否不止一个起始虚拟地址？
    每个进程task_struct中   只有一个mm_struct（对整个地址空间的整体描述   内存描述符）   会有多个 vm_area_struct
    进程会维护一张vm_area_struct的链表，链表中会记录下子区域所要的start/end，堆区不连续也不影响，分段的方式，会把堆区分很多份，一份堆区
对应一张 vm_area_strcut     start/end都知道



进程具有独立性：
    1.内核数据结构独立
    2.加载进入内存的代码和数据独立




进程控制
    * 进程创建
    * 进程终止
    * 进程等待
    * 进程程序替换
    * 自主Shell命令行解释器

进程调用fork，当控制转移到内核中的fork代码后，内核做：
      1.分配新的内存块和内核数据结构给子进程
      2.将父进程部分数据结构内容拷贝至子进程
      3.添加子进程到系统进程列表当中
      4.fork返回，开始调度器调度


写时拷贝
    父子代码共享，父子在不写入时，数据是共享的，当任意一方试图写入，边以写时拷贝的方式各自一份副本
基本原理：
    创建子进程时，拷贝页表时操作系统会把页表当中的数据项改成只读的，写入时就会报错，操作系统就会以报错的形式来进行去触发重新申请内存
    拷贝数据，建立映射关系的操作


为什么要写时拷贝？
    1.减少创建时间
    2.减少内存的浪费


进程创建用的接口叫fork   同一个pid但是有两个值
    同一个虚拟地址值，映射到了不同的物理地址处，内容就不一样，每一个进程都有独立的页表虚拟地址一样但是物理地址不一样，
    地址一样打出来的值不一样。
    fork做了就是把父进程的所有PCB  地址空间  页表相关的结构给子进程拷贝一份，系统中相当于多了一个进程，浅拷贝模式，会导致
    父子内核数据结构不同（有独立PCB），页表不同，地址空间不同，但是默认指向同样的父和子的代码和数据的，fork之后代码默认是共享的，
    但是一旦发现有修改任何一个进程，就会写时拷贝。


fork常规用法：
    1.父进程希望复制自己，使父子进程同时执行不同的代码段
    2.一个进程要执行一个不同的程序

fork调用失败：
    1.系统中有太多进程
    2.实际用户进程数超过了限制
    



进程终止
    进程终止本质：释放系统资源，释放进程申请的相关内核数据结构和对应的数据和代码

进程退出（三种情况   未来给父进程一个交代）
    1.代码运行完毕，代码正确
    2.代码运行完毕，结果不正确     对或者不对由进程的退出码决定
    3.代码异常终止
子进程也是进程 --> 父进程创建 --> 子进程执行的结果

main函数的返回值，通常表明程序的执行情况（前两种情况  0/非0）
对于0/非0 ---> 不同的值表明不同的出错原因

echo  $?   打印最近一个程序（进程）退出时的退出码 （main函数的返回值叫退出时的退出码）   
           进程的退出码是要写到进程的 task_struct 内部的  （只是最近一个程序）


当返回值退出码，被指定的时候，如果是上面的1.2情况    就会返回指定的退出码

但是如果，一旦程序是第三种情况：也就是代码异常终止，退出码返回值没有意义！！！
进程一旦出现异常，一般是进程收到了信号


！进程退出 ！
main （return）
exit(status (退出码))      #include<stdlib.h>           库函数
_exit(status（退出码）)    系统带的                     系统调用

exit       _exit     二者有区别
区别：
    1.进程如果是exit退出的时候，exit(),进程退出的时候，会进行缓冲区的刷新
    2.如果是 _exit退出的时候，_exit(),进程退出的时候，不会进行缓冲区的刷新
   库函数和系统调用实际是上下层的关系     库函数调用的时候会调用对应的系统调用去完成相关问题
*  一直来说的 缓冲区 一定不是操作系统内部的缓冲区！！！
   缓冲区实际是库缓冲区，C语言提供的缓冲区



打印消息不带\n  在缓冲区要刷新   
    *  main函数结束表示进程结束
       其他函数，只表示自己函数调用完成，然后返回
    *  任何地方调用exit，表示进程结束
       函数后续代码不执行，函数不返回，
       直接结束进程，并返回给父进程bash的子进程的退出码




进程等待
    进程等待的必要性（为什么要等待？）
     *   1.子进程退出，父进程不管，就会导致 "僵尸进程" ,导致内存泄漏
         2.一旦进程变成僵尸状态，无法杀死
     *   3.父进程给子进程的任务完成如何我们需要知道，譬如：子进程运行完成，结果对或不对，或者是否正常退出
         4.父进程通过进程等待的方式，回收子进程资源，获取子进程退出信息
                                       （最重要）       （可有可无）
    解决僵尸进程问题，获取子进程退出信息

    进程等待是啥？
        pid_t  wait(int* status);
        pid_t  waitpid(pid_t pid,int* status,int options);
    wait 是等待任意个退出的子进程，返回的pid_t 是目标Z进程的pid（僵尸进程）
*   如果等待子进程，子进程没有退出，父进程就会阻塞在wait调用处（譬如：scanf）
    
       pid_t rid = wait(NULL);
       pid_t rid = waitpid(-1,NULL,0);    //这样和上面是一样的效果   等待成功的返回值就是子进程pid
    等待指定进程
       pid_t rid = waitpid(id,NULL,0);   //指定对应的进程


int* status   输出型参数
实际就是进程退出码，即main返回值
(status>>8)&0xFF       获取退出码       右移八位值不变
WEXITSTATUS(status)    和上面是一个样的（这个是相当于加了一层封装）   若WIFEXITED非0  提取子进程退出码（查看进程的退出码）
WIFEXITED(status)      若正常终止子进程返回的状态   为真  （查看进程是否是正常退出）


进程具有独立性（只能通过系统调用waitpid   才能拿到子进程信息   如果定义全局变量  父进程也是拿不到子进程的信息）

一个字节   
高的十六位对应的是  15---8（退出状态   退出码）      故右移八位 

代码异常终止
    保存异常时对应的信号编号
没有异常的话
    1.低7个比特位的信号值   为0    本是没有0号信号
    2.一旦低七个比特位为  !0   不是0，异常退出的，退出码就没有意义（譬如接收到9号新号杀掉了子进程   退出码就无意义，后面的信号回9）



pid_t waitpid(pid_t pid,int *status,int options)    options ---> WNOHANG(非阻塞)
    waitpid ---> 非阻塞调用

pid_t
    大于0：等待结束
    等于0：调用结束，但是子进程还没有退出
    小于0：等待失败
阻塞式调用只有 大于 和 小于
非阻塞式调用有 大于  小于   等于

非阻塞轮询 ---> 循环完成
    非阻塞调用的好处：可以让等待方做自己的事情！（效率高）   Non Block

怎么让父进程做其他事？
    std::function<>

非阻塞，可以让父进程做其他的事情

*/